#include <QtCore>
#include <QtGui>
#include <QtNetwork>
#ifdef Q_OS_WIN32
#include <Windows.h>
#endif

#include "mixiVoice.h"
#include "ui_mixiVoice.h"
#include "CookieJar.h"
#include "SettingDialog.h"

const QString Setting::Version = "General/Version";
const QString Setting::EMail = "Account/E-Mail";
const QString Setting::Password = "Account/Password";
const QString Setting::SavePassword = "Account/SavePassword";
const QString Setting::ReloadInterval = "General/ReloadInterval";
const QString Setting::AlwaysOnTop = "General/AlwaysOnTop";
const QString Setting::NotificationShowTime = "General/NotificationShowTime";
const QString Setting::NotificationSoundPath = "Sound/Path";
const QString Setting::NotificationSoundEnabled = "Sound/Enabled";
const QString Setting::WindowSize = "Window/Size";
const QString Setting::WindowPosition = "Window/Position";
const QString Setting::VoiceFormat = "Voice/Format";
const QString Setting::WindowBackgroundColor = "Window/BackgroundColor";
const QString Setting::WindowTextColor = "Window/TextColor";
const QString Setting::MaxVoiceCount = "Voice/MaxCount";
const int Setting::MinReloadIntervalInMin = 5;
const QString Setting::HiddenUsers = "Voice/HiddenUser";
const QString Setting::DateTimeFormat = "Voice/DateTimeFormat";

QString mixiVoice::htmlToPlainText(QString html)
{
	return html.replace(QRegExp("<a href=\"[^\"]+\">([^<]+)</a>"), "\\1").replace(QRegExp("<img[^>]+alt=\"([^\"]+)\"[^>]*>"), "(\\1)").replace("&gt;", ">").replace("&lt;", "<").replace("&amp;", "&");
}

QByteArray mixiVoice::encrypt(const QByteArray &s)
{
	QByteArray t;
	for (int i = 0; i != s.size(); ++i)
		t.append(static_cast<char>(~s[i]));
	return t;
}

QByteArray mixiVoice::decrypt(const QByteArray &s)
{
	return encrypt(s);
}

mixiVoice::mixiVoice(QWidget *parent)
	: QWidget(parent), ui(new Ui::mixiVoice)
{
	if (QFileInfo(qApp->applicationName() + ".ini").exists())
		settings = new QSettings(qApp->applicationName() + ".ini", QSettings::IniFormat, this);
	else
		settings = new QSettings(QSettings::IniFormat, QSettings::UserScope, qApp->organizationName(), qApp->applicationName(), this);

	setupContextMenu();
	setupUI();

	// setup network
	const QString appDataPath = QString("%1/%2/").arg(QFileInfo(QSettings(QSettings::IniFormat, QSettings::UserScope, qApp->organizationName(), qApp->applicationName(), this).fileName()).path()).arg(qApp->applicationName());
	QDir().mkdir(appDataPath);
	cookieJar = new CookieJar(appDataPath + "cookie.bin");
	netMan = new QNetworkAccessManager(this);
	connect(netMan, SIGNAL(finished(QNetworkReply *)), this, SLOT(replyFinished(QNetworkReply *)));
	netMan->setCookieJar(cookieJar);

	// interval timer
	timer = new QTimer(this);
	timer->setInterval(Setting::MinReloadIntervalInMin * 60 * 1000);
	connect(timer, SIGNAL(timeout()), this, SLOT(reload()));

	// first run
	if (settings->value(Setting::EMail).toString().isEmpty() || settings->value(Setting::Version).toString() != qApp->applicationVersion())
	{
		settings->setValue(Setting::Password, "");
		showSettingDialog();
	}
	else
	{
		loadSettings();
		reload();
	}
}

void mixiVoice::setupUI()
{
	ui->setupUi(this);

	for (int i = 0; i != (sizeof voice / sizeof voice[0]); ++i)
	{
		QLabel *label = new QLabel(this);
		connect(label, SIGNAL(linkActivated(QString)), this, SLOT(openURL(const QString &)));
		label->setWordWrap(true);
		label->hide();
		voice[i] = label;
		this->layout()->addWidget(label);
	}

	// system tray icon
	setupContextMenu();
	trayIcon = new QSystemTrayIcon(this);
	trayIcon->setContextMenu(trayIconMenu);
	trayIcon->setIcon(QIcon(":/Icon/TrayIcon"));
	trayIcon->setToolTip("mixiVoice");
	trayIcon->setVisible(true);
	connect(trayIcon, SIGNAL(messageClicked()), this, SLOT(show()));
	connect(trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, SLOT(trayIconActivated(QSystemTrayIcon::ActivationReason)));
}

mixiVoice::~mixiVoice()
{
	saveSettings();
	delete ui;
}

void mixiVoice::loadSettings()
{
	if (settings->value("General/AlwaysOnTop").toBool())
		setWindowFlags(Qt::Tool | Qt::WindowCloseButtonHint | Qt::WindowStaysOnTopHint);
	else
		setWindowFlags(Qt::Widget | Qt::WindowCloseButtonHint);

	maxVoiceCount = settings->value(Setting::MaxVoiceCount, 10).toInt();
	if (maxVoiceCount < 1)	maxVoiceCount = 1;
	if (static_cast<int>(sizeof voice / sizeof voice[0]) < maxVoiceCount)	maxVoiceCount = 25;

	this->move(settings->value(Setting::WindowPosition, QPoint(this->x(), this->y())).toPoint());
	this->resize(settings->value(Setting::WindowSize, this->size()).toSize());

	QPalette palette;
	palette.setColor(QPalette::Window, QColor(settings->value(Setting::WindowBackgroundColor, "lightgray").toString()));
	palette.setColor(QPalette::WindowText, QColor(settings->value(Setting::WindowTextColor, "black").toString()));
	setPalette(palette);

	timer->stop();
	timer->setInterval(settings->value(Setting::ReloadInterval, 5).toInt() * 60 * 1000);
}

void mixiVoice::saveSettings()
{
	settings->setValue(Setting::WindowSize, this->size());
	settings->setValue(Setting::WindowPosition, QPoint(this->x(), this->y()));
}

void mixiVoice::closeEvent(QCloseEvent *e)
{
	hide();
	e->ignore();
}

void mixiVoice::contextMenuEvent(QContextMenuEvent *e)
{
	const QWidget *l = this->childAt(e->pos());
	const int count = settings->value(Setting::MaxVoiceCount).toInt();
	for (selectedVoiceIndex = 0; selectedVoiceIndex != count; ++selectedVoiceIndex)
		if (l == voice[selectedVoiceIndex])
			break;

	actDeleteSelectedVoice->setEnabled(voices[selectedVoiceIndex].isDeletable);
	contextMenu->exec(e->globalPos());
	e->accept();
}

void mixiVoice::setupContextMenu()
{
	// setup tray icon context menu
	trayIconMenu = new QMenu(this);

	QAction *a;

	a = new QAction(tr("Show &Window"), this);
	connect(a, SIGNAL(triggered()), this, SLOT(showNormal()));
	trayIconMenu->addAction(a);

	a = new QAction(tr("&Reload"), this);
	connect(a, SIGNAL(triggered()), this, SLOT(reload()));
	trayIconMenu->addAction(a);

	trayIconMenu->addSeparator();

	a = new QAction(tr("&About"), this);
	connect(a, SIGNAL(triggered()), this, SLOT(about()));
	trayIconMenu->addAction(a);

	trayIconMenu->addSeparator();

	a = new QAction(tr("&Settings"), this);
	connect(a, SIGNAL(triggered()), this, SLOT(showSettingDialog()));
	trayIconMenu->addAction(a);

	a = new QAction(tr("&Quit"), this);
	connect(a, SIGNAL(triggered()), qApp, SLOT(quit()));
	trayIconMenu->addAction(a);

	// setup window context menu
	contextMenu = new QMenu(this);

	a = new QAction(tr("Reload"), this);
	connect(a, SIGNAL(triggered()), this, SLOT(reload()));
	contextMenu->addAction(a);

	contextMenu->addSeparator();

	a = new QAction(tr("&Reply to This Voice"), this);
	connect(a, SIGNAL(triggered()), this, SLOT(replyToSelectedVoice()));
	contextMenu->addAction(a);

	a = new QAction(tr("Post &New Voice"), this);
	connect(a, SIGNAL(triggered()), this, SLOT(postNewVoice()));
	contextMenu->addAction(a);

	contextMenu->addSeparator();

	actDeleteSelectedVoice = a = new QAction(tr("&Delete This Voice"), this);
	connect(a, SIGNAL(triggered()), this, SLOT(deleteSelectedVoice()));
	contextMenu->addAction(a);

	a = new QAction(tr("Hide This User"), this);
	connect(a, SIGNAL(triggered()), this, SLOT(addSelectedUserToHiddenList()));
	contextMenu->addAction(a);

	contextMenu->addSeparator();

	a = new QAction(tr("&Settings"), this);
	connect(a, SIGNAL(triggered()), this, SLOT(showSettingDialog()));
	contextMenu->addAction(a);
}

void mixiVoice::addSelectedUserToHiddenList()
{
	if (selectedVoiceIndex == settings->value(Setting::MaxVoiceCount).toInt())	return;

	QStringList list = settings->value(Setting::HiddenUsers).toStringList();
	const QString userIDString = QString("%1").arg(voices[selectedVoiceIndex].userID);

	if (list.contains(userIDString))	return;

	list << userIDString;
	settings->setValue(Setting::HiddenUsers, list);

	reload();
}

void mixiVoice::deleteSelectedVoice()
{
	if (selectedVoiceIndex == settings->value(Setting::MaxVoiceCount).toInt())	return;

	const QString title = tr("Confirmation of Delete a Voice");
	const QString content = tr("Are you sure to delete this voice?\n\"%1\"").arg(htmlToPlainText(voices[selectedVoiceIndex].content));
	if (QMessageBox::No == QMessageBox::question(this, title, content, QMessageBox::Yes | QMessageBox::No, QMessageBox::No))	return;

	if (postKey.isEmpty())	return;
	const QString postTime = voices[selectedVoiceIndex].dateTimeString();
	netMan->get(QNetworkRequest(QUrl(QString("http://mixi.jp/delete_echo.pl?post_time=%1&post_key=%2").arg(postTime).arg(postKey))));
}

void mixiVoice::replyToSelectedVoice()
{
	if (selectedVoiceIndex == settings->value(Setting::MaxVoiceCount).toInt())	return;

	QString content = QInputDialog::getText(this, tr("Reply to Voice"), tr("Content:"));
	if (content.isNull())	return;

	if (postKey.isEmpty())	return;
	const int userID = voices[selectedVoiceIndex].userID;
	const QString postTime = voices[selectedVoiceIndex].dateTimeString();
	const QByteArray postData(QString("parent_member_id=%1&parent_post_time=%2&post_key=%3&body=%4&redirect=recent_echo").arg(userID).arg(postTime).arg(postKey).arg(QString(QTextCodec::codecForName("EUC-JP")->fromUnicode(content).toPercentEncoding())).toAscii());
	netMan->post(QNetworkRequest(QUrl("http://mixi.jp/add_echo.pl")), postData);
}

void mixiVoice::changeEvent(QEvent *e)
{
	// qDebug() << "event type:" << e->type();
	switch (e->type())
	{
	case QEvent::ActivationChange:
		trayIcon->setIcon(QIcon(":/Icon/TrayIcon"));
	default:
		QWidget::changeEvent(e);
	}
}

void mixiVoice::trayIconActivated(QSystemTrayIcon::ActivationReason reason)
{
	switch (reason)
	{
/*
		case QSystemTrayIcon::Trigger:
			reload();
			break;
*/

		case QSystemTrayIcon::DoubleClick:
			if (this->isVisible())
				hide();
			else
				show();
			trayIcon->setIcon(QIcon(":/Icon/TrayIcon"));
			break;

		default:
			break;
	}
}

void mixiVoice::login()
{
	if (settings->value(Setting::Password, QString()).toString().isEmpty())
	{
		reload();
		return;
	}

	cookieJar->clear();
	const QString email = settings->value(Setting::EMail).toString().replace('@', "%40");
	const QString password = decrypt(settings->value(Setting::Password).toByteArray());
	const QByteArray postData(QString("next_url=/home.pl&email=%1&password=%2&sticky=sticky").arg(email).arg(password).toAscii());
	netMan->post(QNetworkRequest(QUrl("http://mixi.jp/login.pl")), postData);

	if (settings->value(Setting::SavePassword, false).toBool() == false)
		settings->setValue(Setting::Password, QString());
}

void mixiVoice::reload()
{
	netMan->get(QNetworkRequest(QUrl("http://mixi.jp/recent_echo.pl")));
	timer->stop();
	timer->start();
}

void mixiVoice::showSettingDialog()
{
	SettingDialog d(settings, this);
	if (d.exec() == QDialog::Rejected)	return;
	saveSettings();
	loadSettings();
	show();
	clearAllVoices();
	login();
}

void mixiVoice::replyFinished(QNetworkReply *r)
{
	const QString url = r->url().toString();
	const QByteArray plainContent = r->readAll();
	const QString content = QTextCodec::codecForName("EUC-JP")->toUnicode(plainContent);

	qDebug() << url;

	if (url.endsWith("login.pl"))	// login
	{
		if (content.contains("refresh"))
		{
			trayIcon->showMessage(tr("mixiVoice"), tr("Login Successful"), QSystemTrayIcon::Information, 5 * 1000);
			reload();
		}
		else
		{
			QMessageBox::warning(this, tr("mixi Login"), tr("Failed to login.\nPlease check your e-mail address and password."));
		}
		return;
	}
	else if (url.contains("recent_echo.pl") && content.contains("/login.pl"))
	{
		// ログインしてない状態
		if (settings->value(Setting::Password).toString().isEmpty())
		{
			showSettingDialog();
		}
		else
		{
			trayIcon->showMessage(tr("mixiVoice"), tr("Logging in mixi..."));
			login();
		}
		return;
	}
	else if (url.contains("add_echo.pl") || url.contains("delete_echo.pl"))
	{
		// 新規ボイス投稿後、ボイス削除後に更新
		reload();
		return;
	}
	else if (url.endsWith(".jpg"))	// user pictures
	{
		QRegExp re("http://profile\\.img\\.mixi\\.jp/photo/member/\\d+/\\d+/(\\d+)_\\d+m\\.jpg");
		if (re.indexIn(url, 0) == -1)	return;
		const QString id = re.cap(1);
		QFile file(QString("%1/mixiVoice_%2.jpg").arg(QDir::tempPath()).arg(id));
		if (file.open(QIODevice::WriteOnly))
			file.write(plainContent);
		else
			qDebug() << "Failed to open file.";
		if (--loadingImage == 0)	reload();	// reload when all user pictures are saved
		return;
	}
	else if (url.endsWith(".gif"))
	{
		QRegExp re("http://img\\.mixi\\.jp/img/emoji/(\\d+).gif");
		if (re.indexIn(url, 0) == -1)	return;
		const QString id = re.cap(1);
		QFile file(QString("%1/mixiVoice_%2.gif").arg(QDir::tempPath()).arg(id));
		if (file.open(QIODevice::WriteOnly))
			file.write(plainContent);
		else
			qDebug() << "Failed to open file.";
		if (--loadingImage == 0)	reload();	// reload when all images are saved
		return;
	}

	const int sIndex = content.indexOf("<div class=\"archiveList\">");
	const int eIndex = content.lastIndexOf("<!--[Echo]-->");
	const QString echoContent = content.mid(sIndex, eIndex - sIndex);
	/* get post key */
	QRegExp pkRE("<input type=\"hidden\" name=\"post_key\" id=\"post_key\" value=\"(\\w+)\" />");
	if (pkRE.indexIn(content, 0) != -1)
		postKey = pkRE.cap(1);

	// parse voices
	QRegExp re("<td class=\"thumb\"><a href=\"list_echo\\.pl\\?id=(\\d+)\"><img src=\"([^\"]+)\" alt=\"([^\"]+)\" /></a></td>\\n" \
			   "<td class=\"nickname\"><a href=\"list_echo\\.pl\\?id=\\d+\">[^<]+</a></td>\\n" \
			   "<td class=\"comment\">([^\\n]+)\\n" \
			   "(<a href=\"[^\"]+\" class=\"trash\"><img src=\"[^\"]+\" alt=\"[^\"]+\" title=\"[^\"]+\" /></a>)?\\n" \
			   "<div class=\"echo_member_id\" style=\"display: none;\">\\d+</div>\\n" \
			   "<div class=\"echo_post_time\" style=\"display: none;\">(\\d+)</div>\\n");
	QList<Voice> voices;
	const QStringList hiddenUserList = settings->value(Setting::HiddenUsers).toStringList();

	for (int i = loadingImage = 0; (i = re.indexIn(echoContent, i)) != -1; i += re.matchedLength())
	{
		// skip hidden user
		const QString idString = re.cap(1);
		if (hiddenUserList.contains(idString))	continue;

		int id = idString.toInt();	// ID
		const QString picURL = re.cap(2);	// Picture
		const QString name = re.cap(3);	// Name
		const QString comment = re.cap(4).replace(QRegExp("<span>.+"), "").replace(QRegExp("<a href=\"[^\"]+\">([^<]+)</a>"), "\\1 ").replace("<wbr/>", "").replace(" target=\"_blank\"", "").replace(QRegExp("<img src=\"http://img\\.mixi\\.jp/img/emoji/(\\d+)\\.gif\" alt=\"([^\"]+)\"[^>]*>"), QString("<img src=\"%1/mixiVoice_\\1.gif\" alt=\"\\2\" />").arg(QDir::tempPath()));
		const bool deletable = !re.cap(5).isEmpty();
		const QString dtString = re.cap(6);
		const QDateTime dt = QDateTime::fromString(dtString, "yyyyMMddHHmmss");

		voices.push_back(Voice(id, name, comment, dt, deletable));

		// request user picture
		if (picURL.endsWith(".jpg") && !QFileInfo(QString("%1/mixiVoice_%2.jpg").arg(QDir::tempPath()).arg(id)).exists())
		{
			netMan->get(QNetworkRequest(QUrl(picURL)));
			++loadingImage;
		}

		// request emoji
		QRegExp emojiRE(QString("<img src=\"%1/mixiVoice_(\\d+).gif\"[^>]*>").arg(QDir::tempPath()));
		for (int j = 0; (j = emojiRE.indexIn(comment, j)) != -1; j += emojiRE.matchedLength())
		{
			int emojiID = emojiRE.cap(1).toInt();
			if (QFileInfo(QString("%1/mixiVoice_%2.gif").arg(QDir::tempPath()).arg(emojiID)).exists())	continue;
			netMan->get(QNetworkRequest(QUrl(QString("http://img.mixi.jp/img/emoji/%1.gif").arg(emojiID))));
			++loadingImage;
		}
	}

	this->setVoices(voices);
}

void mixiVoice::setVoices(const QList<Voice> &list)
{
	voices = list;
	const QString format = settings->value(Setting::VoiceFormat).toString();

	/* find new voices */
	const int notificationShowTime = settings->value(Setting::NotificationShowTime, 5).toInt();
	int newVoice = 0;
	if (notificationShowTime != 0 && !voice[0]->text().isEmpty())
	{
		for (int i = 0; i != list.size(); ++i)
		{
			const QString content = voiceStringWithFormat(list[i], format);
			if (voice[0]->text() == content)
			{
				newVoice = i;
				break;
			}
		}

		// generate tray message
		QString message;
		for (int i = 0; i != newVoice; ++i)
			message += tr("%1: %2\n\n").arg(list[i].userName).arg(htmlToPlainText(list[i].content));
		if (!message.isEmpty())
		{
			trayIcon->showMessage(tr("New Voices"), message, QSystemTrayIcon::Information, notificationShowTime * 1000);
			if (settings->value(Setting::NotificationSoundEnabled).toBool())
			{
				QSound sound(settings->value(Setting::NotificationSoundPath).toString());
				sound.play();
			}
		}
	}

	// flip tray icon when new voice exists
	if (newVoice && !this->isActiveWindow())	trayIcon->setIcon(QIcon(":/Icon/TrayIcon_inverted"));

	/* update view */
	for (int i = 0; i != maxVoiceCount && i != (sizeof voice / sizeof voice[0]) && i != list.size(); ++i)
	{
		const QString content = voiceStringWithFormat(list[i], format);
		voice[i]->show();
		voice[i]->setText(content);
	}
#ifdef Q_OS_WIN32
	this->resize(this->width(), 1);
#else
	this->adjustSize();
#endif
}

QString mixiVoice::voiceStringWithFormat(const Voice &v, QString f) const
{
	// %a: application name
	// %u: user id
	// %n: user name
	// %c: content
	// %t: post time
	// %d: image dir path
	return f
			.replace("%a", qApp->applicationName())
			.replace("%u", QString("%1").arg(v.userID))
			.replace("%n", v.userName)
			.replace("%t", v.dateTime.toString(settings->value(Setting::DateTimeFormat).toString()))
			.replace("%d", QDir::tempPath())
			.replace("%c", v.content);
}

void mixiVoice::clearAllVoices()
{
	for (int i = 0; i != (sizeof voice / sizeof voice[0]); ++i)
	{
		voice[i]->clear();
		voice[i]->hide();
	}
}

void mixiVoice::postNewVoice()
{
	QString content = QInputDialog::getText(this, tr("New Voice"), tr("Content:"));
	if (content.isNull())	return;

	if (postKey.isEmpty())	return;
	const QByteArray postData(QString("post_key=%1&body=%2&redirect=recent_echo").arg(postKey).arg(QString(QTextCodec::codecForName("EUC-JP")->fromUnicode(content).toPercentEncoding())).toAscii());
	netMan->post(QNetworkRequest(QUrl("http://mixi.jp/add_echo.pl")), postData);
}

void mixiVoice::openURL(const QString &url)
{
/*
	const QString title = tr("Opening an URL");
	const QString content = tr("Are you sure to open this URL?");
	if (QMessageBox::No == QMessageBox::question(this, title, content, QMessageBox::Yes | QMessageBox::No, QMessageBox::No))	return;
*/
	QDesktopServices::openUrl(QUrl(url));
}

void mixiVoice::about()
{
	QString message = tr("%1 %2\nby %3\nQt Version: %4\nCompiled with gcc %5.%6.%7 on %8")
					  .arg(qApp->applicationName())
					  .arg(qApp->applicationVersion())
					  .arg(qApp->organizationName())
					  .arg(QT_VERSION_STR)
					  .arg(__GNUC__)
					  .arg(__GNUC_MINOR__)
					  .arg(__GNUC_PATCHLEVEL__)
					  .arg(__DATE__);
	QMessageBox::information(this, tr("About"), message);
}

#ifdef Q_OS_WIN32
bool mixiVoice::winEvent(MSG *message, long *result)
{
	if (message->message == WM_POWERBROADCAST)
	{
		switch (message->wParam)
		{
		case PBT_APMRESUMESUSPEND:
			timer->start();
			break;
		case PBT_APMSUSPEND:
			timer->stop();
			break;
		default:
			break;
		}
	}
	return QWidget::winEvent(message, result);
}
#endif
